package cn.itcast.nems.common.web.controller.b;

import cn.hutool.core.lang.Assert;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.itcast.ic.common.exception.v2.BizExceptionProducer;
import cn.itcast.ic.common.exception.v2.bean.BizErrorBodyEnum;
import cn.itcast.nems.common.enumeration.NemsErrorBodyEnum;
import cn.itcast.nems.manager.kc.KeyCloakConstants;
import cn.itcast.nems.manager.redis.RedisUtil;
import cn.itcast.nems.manager.redis.constant.RedisKeyConstant;
import cn.itcast.nems.manager.util.CustomBeanUtil;
import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ResponseStatusException;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController()
@RequestMapping("/b/kc")
public class KeycloakController {
    /**
     * spring 模板请求类
     */
    @Autowired
    private RestTemplate restTemplate;
    /**
     * keycloak api地址
     */
    @Value("${ems2.keycloak.b-api.base-url}")
    private String keyCloakBaseUrl;
    /**
     * keycloak client-admin-url地址
     */
    @Value("${ems2.keycloak.b-api.client-admin-url}")
    private String keyCloakAdminUrl;
    /**
     * keycloak client-admin-url地址
     */
    @Value("${ems2.keycloak.b-api.admin-username}")
    private String keyCloakAdminUsername;
    /**
     * keycloak client-admin-url地址
     */
    @Value("${ems2.keycloak.b-api.admin-password}")
    private String keyCloakAdminPassword;
    /**
     * keycloak api地址
     */
    @Value("${ems2.keycloak.b-api.realm}")
    private String keyCloakRealm;
    /**
     * keycloak 应用id
     */
    @Value("${ems2.keycloak.b-api.client-id}")
    private String keyCloakClientId;
    /**
     * keycloak 应用秘钥
     */
    @Value("${ems2.keycloak.b-api.client-secret}")
    private String keyCloakClientSecret;
    /**
     * redis缓存工具类 TODO
     */
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 退出登录
     *
     * @param authorization token
     */
    @RequestMapping("/logout")
    public ResponseEntity<Boolean> logout(String authorization) throws Exception {

        Assert.isTrue(StringUtils.isNotEmpty(authorization), BizExceptionProducer.produce(NemsErrorBodyEnum.PARAM_EMPTY,"authorization"));
        authorization = authorization.replace(KeyCloakConstants.BEARER, "");
        // 验证token
        JSONObject jsonObject = this.handleValidToken(authorization);
        boolean active = jsonObject.getBool(KeyCloakConstants.TOKEN_ACTIVE);
        Assert.isTrue(active, BizExceptionProducer.produce(BizErrorBodyEnum.TOKEN_EXPRIRE));
        // 退出登录
        String sessionId = jsonObject.getStr(KeyCloakConstants.TOKEN_SESSION_STATE);
        this.logoutBySessionId(sessionId);
        return ResponseEntity.ok().body(true);
    }

    /**
     * 校验token是否有效
     *
     * @param token 要校验的token
     * @return true 有效
     */
    protected JSONObject handleValidToken(String token) {
        if (token.isEmpty()) {
            // 参数缺失
            return new JSONObject();
        }
        // 请求KeyCloak通用token
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
        paramMap.add("client_id", keyCloakClientId);
        paramMap.add("client_secret", keyCloakClientSecret);
        paramMap.add("token", token);
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(paramMap, requestHeaders);
        ResponseEntity<String> responseEntity = restTemplate.postForEntity(
                keyCloakBaseUrl + "/auth/realms/" + keyCloakRealm + "/protocol/openid-connect/token/introspect", requestEntity,
                String.class);
        log.info("handleValidToken 响应：{}", responseEntity);

        // 解析响应
        return parseKeyCloakResponse(responseEntity);
    }

    /**
     * 解析KeyCloak响应信息
     *
     * @param response 响应
     * @return 响应信息
     */
    private JSONObject parseKeyCloakResponse(ResponseEntity<String> response) {
        final String body = response.getBody();
        var result = JSONUtil.parseObj(body);
        if (!response.getStatusCode().is2xxSuccessful()) {
            String errorDescription = result.getStr("error_description");
            log.error("解析KeyCloak响应信息：错误信息为：{}", errorDescription);
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请求错误，请关闭窗口稍后重试");
        }
        return result;
    }

    /**
     * 通过sessionId退出登录
     *
     * @param sessionId KeyCloak sessionId
     */
    public void logoutBySessionId(String sessionId) throws Exception {
        // 获取管理用户的AccessToken
        String adminUserAccessToken = getAdminUserAccessToken();
        String clientAdminSessionOptUrl = keyCloakAdminUrl + keyCloakRealm + "/sessions/" + sessionId;
        // 调用接口
        HttpResponse httpResponse = cn.hutool.http.HttpRequest.delete(clientAdminSessionOptUrl)
                // 头信息，多个头信息多次调用此方法即可
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                .header(HttpHeaders.AUTHORIZATION, KeyCloakConstants.BEARER + adminUserAccessToken)
                // 超时，毫秒
                .timeout(5000).execute();
        log.info("logoutBySessionId，响应信息：{}", httpResponse);

        if (!httpResponse.isOk()) {
            log.error("通过sessionId退出登录：{} 失败，响应结果：{}", sessionId, httpResponse);
            BizExceptionProducer.throwProduce(NemsErrorBodyEnum.LOGOUT_ERROR);
        }
    }

    /**
     * 获取当前admin用户的登录AccessToken
     *
     * @return AccessToken
     */
    private String getAdminUserAccessToken() {
        // 从缓存中获取KeyCloak AccessToken
        String adminAccessToken = null;
        String redisKey = CustomBeanUtil.append("|",RedisKeyConstant.NEMS_KEYCLOAK_ADMIN_ACCESS_TOKEN ,keyCloakClientId) ;
        try {
            if (redisUtil.hasKey(redisKey)) {
                // 有则返回
                adminAccessToken  =  (String) redisUtil.get(redisKey)   ;
                return adminAccessToken;
            }
            // 如果未找到缓存则重新登录
            JSONObject loginResultObject = loginByKeyCloakPasswordModel();
            adminAccessToken = loginResultObject.getStr(KeyCloakConstants.TOKEN_ACCESS_TOKEN);
            // 过期时间(秒)
            Integer expiresIn = loginResultObject.getInt(KeyCloakConstants.TOKEN_EXPIRES_IN);
            // 缓存 KeyCloak通用admin token
            redisUtil.set(redisKey, adminAccessToken,  (expiresIn > 500 ? expiresIn - 500L : expiresIn));
        } catch (Exception e) {
            log.error("获取当前用户的登录AccessToken 异常：{}" ,e.getMessage());
            redisUtil.del(redisKey) ;
        }

        return adminAccessToken;
    }

    /**
     * KeyCloak 密码模式登录
     *
     * @return 登录结果
     */
    protected JSONObject loginByKeyCloakPasswordModel() {
        // 请求参数
        Map<String, Object> paramMap = new HashMap<>(5);
        paramMap.put(KeyCloakConstants.CLIENT_ID, keyCloakClientId);
        paramMap.put(KeyCloakConstants.GRANT_TYPE, KeyCloakConstants.GRANT_TYPE_PASSWORD);
        paramMap.put(KeyCloakConstants.CLIENT_SECRET, keyCloakClientSecret);
        paramMap.put(KeyCloakConstants.USERNAME, keyCloakAdminUsername);
        paramMap.put(KeyCloakConstants.PASSWORD, keyCloakAdminPassword);

        HttpResponse httpResponse = cn.hutool.http.HttpRequest
                .post(keyCloakBaseUrl + "/auth/realms/" + keyCloakRealm + "/protocol/openid-connect/token")
                // 头信息，多个头信息多次调用此方法即可
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                // 表单内容
                .form(paramMap)
                // 超时，毫秒
                .timeout(5000).execute();

        String responseStr = httpResponse.body();
        JSONObject responseObject = JSONUtil.parseObj(responseStr);

        if (responseObject.isEmpty() || !httpResponse.isOk()) {
            log.error("密码模式登录失败，响应信息为：{}", httpResponse);
            BizExceptionProducer.throwProduce(BizErrorBodyEnum.SERVICE_ERROR);
        }
        // 错误处理
        String error = responseObject.getStr(KeyCloakConstants.ERROR);
        if (StringUtils.isNotBlank(error)) {
            log.error("密码模式登录失败，用户名：{} ，失败原因：{}", keyCloakAdminUsername, responseObject);
            if (KeyCloakConstants.ERROR_LOGIN_FAIL.equals(responseObject.getStr(KeyCloakConstants.ERROR_DESCRIPTION))) {
                BizExceptionProducer.throwProduce(BizErrorBodyEnum.SERVICE_ERROR);
            }
            BizExceptionProducer.throwProduce(BizErrorBodyEnum.SERVICE_ERROR);
        }
        return responseObject;
    }


}